package nl.unimaas.games.tron.experiment;

import java.awt.Color;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Random;
import java.util.logging.Logger;

import javax.swing.filechooser.FileFilter;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.simpleframework.xml.Attribute;
import org.simpleframework.xml.Element;
import org.simpleframework.xml.Root;
import org.simpleframework.xml.Serializer;
import org.simpleframework.xml.core.Persister;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import nl.unimaas.games.tron.engine.Board;
import nl.unimaas.games.tron.engine.Game;
import nl.unimaas.games.tron.engine.GameListener;
import nl.unimaas.games.tron.player.Player;

public class Experiment implements GameListener {
	private final static int DEFAULT_TURN_LIMIT = 0;
	private final static int DEFAULT_TURN_TIME_LIMIT = 1000;//Integer.MAX_VALUE;
	private final static int DEFAULT_REPEAT = 1;
	private final static boolean DEFAULT_SELFPLAY = false;
	private final static boolean DEFAULT_BOTHPOS = true;
	private final static boolean DEFAULT_SCRAMBLE = false;
	private final static boolean DEFAULT_PARALLEL = false;
	private final static boolean DEFAULT_ANALYZE = false;
	private final static Random random = new Random(System.nanoTime());
	
	private String id, description;
	private String resultFile = "";
	private String baseDir = "";
	private Player[] players;
	private Board[] boards;
	
	private boolean parallelMode = DEFAULT_PARALLEL;
	private boolean analyze = DEFAULT_ANALYZE;
	private int turnLimit = DEFAULT_TURN_LIMIT;
	private int turnTimeLimit = DEFAULT_TURN_TIME_LIMIT;
	private int repeat = DEFAULT_REPEAT;
	private boolean selfPlay = DEFAULT_SELFPLAY;
	private boolean bothPos = DEFAULT_BOTHPOS;
	private boolean scramblePositions = DEFAULT_SCRAMBLE;
	
	private Game game;
	private Logger logger;
	
	public boolean DEBUG_OUTPUT = false;
	
	public Experiment(String id, String description, Player[] players, Board[] boards, 
			int turnLimit, int turnTimeLimit, boolean scramblePositions, boolean selfPlay, boolean bothPos, int repeat) {
		this.id = id;
		this.description = description;
		this.players = players;
		this.boards = boards;
		this.turnLimit = turnLimit;
		this.turnTimeLimit = turnTimeLimit;
		this.scramblePositions = scramblePositions;
		this.selfPlay = selfPlay;
		this.bothPos = bothPos;
		this.repeat = repeat;
	}

	/** Runs this experiment on the current thread */
	public void run() {
		//generate match list
		ArrayList<Player[]> matches = new ArrayList<Player[]>();

		logger.info(" - Matches  -");
		for (int i = 0; i < players.length; i++) {
			for (int ii = i; ii < players.length; ii++) {
				if (!selfPlay && i == ii)
					continue;
				matches.add(new Player[] {players[i], players[ii]});
				logger.info(String.format("\t %s vs %s", players[i].toDescriptiveString(), players[ii].toDescriptiveString()));
			}
		}
		
		if (bothPos) {
			logger.info(String.format(" Swapped position:"));
			for (int i = 0; i < players.length; i++) {
				for (int ii = i; ii < players.length; ii++) {
					if (!selfPlay && i == ii)
						continue;
					matches.add(new Player[] {players[ii], players[i]});
					logger.info(String.format("\t %s vs %s", players[ii].toDescriptiveString(), players[i].toDescriptiveString()));
				}
			}
		}
		logger.info("Total match count of " + matches.size());
		
		logger.info(" - Boards: -");
		for (int i = 0; i < boards.length; i++)
			logger.info(String.format("\t %s", boards[i].getName()));
		
		int counter = 0;
		GameAnalyzer analyzer = null;
		if (analyze)
			analyzer = new GameAnalyzer();
		for (Player[] match : matches) {
			logger.info(String.format("%.2f%% : %s vs %s", (((double) counter++ / (matches.size())) * 100), match[0].toDescriptiveString(), match[1].toDescriptiveString()));
			for (int i_board = 0; i_board < boards.length; i_board++) {
				logger.info(String.format("\t Board %s", boards[i_board].getName()));
				Player p1 = match[0].clone();
				Player p2 = match[1].clone();
				Board b = boards[i_board].deepClone(true);
				Result result = new Result();
				result.games = repeat;
				result.turnLimit = turnLimit;
				result.turnTimeLimit = turnTimeLimit;
				result.scramble = scramblePositions;
				result.player1 = p1.toDescriptiveString();
				result.player2 = p2.toDescriptiveString();
				result.turns = new int[repeat];
				result.gameEnds = new int[repeat];
				result.board = b.getName();
				if (result.board.contains("/")) {
					result.board = result.board.substring(result.board.lastIndexOf("/") + 1);
					if (result.board.endsWith(".txt"))
						result.board = result.board.substring(0, result.board.length() - 4);
				}
				
				for (int i_repeat = 0; i_repeat < repeat; i_repeat++) {
					Board bb = b.deepClone(true);
					result.index = i_repeat;
					runGame(result, 
							p1, p2,
							bb,
							turnLimit, turnTimeLimit,
							scramblePositions, analyzer);
					logger.info(String.format("\t\t [%d/%d] Game end: %d \n", i_repeat + 1, repeat, result.gameEnds[i_repeat]));
				}
				
				try {
					result.save(resultFile);
					if (analyzer != null)
						analyzer.save(resultFile + ".dat");
				}
				catch (Exception e) {
					System.err.println("Unable to save result to '" + resultFile + "': " + e.getMessage());
					e.printStackTrace();
				}
			}
		}
		logger.info("Done running " + id + ": " + description);
	}
	
	private synchronized void runGame(Result result, Player p1, Player p2, Board board, int turnLimit, int turnTimeLimit, boolean scramblePositions, GameAnalyzer analyzer) {
		if (game != null) 
			throw new IllegalStateException("we already have a game");
		
		do {
			p1.reset();
			p2.reset();
			game = new Game(new Player[] {p1, p2}, board.deepClone(true));
			game.setTurnRules((short) turnLimit, turnTimeLimit);
			game.setStartPositionsScrambled(scramblePositions);
	
			if (analyzer != null)
				analyzer.setGame(game);
			logger.fine("Starting game " + game.toLongString());
			game.setLogger(logger);
			game.setComputeMovesInParallel(parallelMode);
			game.addGameListener(this);
			game.setup();
			game.start();
			try {
				wait();
			} catch (InterruptedException e) {}
			//game finished
			if (game.somethingWentWrong)
				System.err.println("Something went wrong in game #" + result.index + ": restarting");
		}
		while (game.somethingWentWrong); //repeat until nothing went wrong
		
		result.turns[result.index] = game.getTurn();
		if (game.isGameOver()) {
			if (game.getWinner() == null) {
				result.gameEnds[result.index] = Result.PLAYER_DRAW;
				result.draws++;
			}
			else if (game.getWinner() == game.getPlayer(0)) {
				result.gameEnds[result.index] = Result.PLAYER1_WON;
				result.wins++;
			}
			else
				result.gameEnds[result.index] = Result.PLAYER2_WON;
		}
		else {
			result.gameEnds[result.index] = Result.GAME_UNDECIDED;
			System.err.println("Game undecided!");
		}
		result.player1Score = result.wins + result.draws * 0.5;
		result.player2Score = result.games - result.player1Score;
		if (analyzer != null)
			analyzer.getStatistics().print();
		game = null;
	}

	public void setLogger(Logger log) {
		this.logger = log;
		if (game != null)
			game.setLogger(log);
	}
	
	@Override
	public String toString() {
		return String.format("Experiment{id=%s, desc=%s}", id, description);
	}
	
	public void setResultDirectory(String dir) {
		this.baseDir = dir;
		DateFormat dateFormat = new SimpleDateFormat("MM.dd-HH.mm.ss");
		String name = String.format("%s(%s) - %s(%d) %d.xml", id, description,
				dateFormat.format(new Date()), random.nextInt(1000), turnTimeLimit);
		resultFile = baseDir + File.separatorChar + name;
	}
	
	public void setParallelMode(boolean enabled) {
		this.parallelMode = enabled;
	}
	
	public void setGameAnalysisMode(boolean enabled) {
		this.analyze = enabled;
	}
	
	public String getId() {
		return id;
	}
	
	public String getDescription() {
		return description;
	}
	
	public static Experiment fromFile(String filePath) {
		try {
			File file = new File(filePath);
			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
			DocumentBuilder db = dbf.newDocumentBuilder();
			Document doc = db.parse(file);
			doc.getDocumentElement().normalize();
			Node root = doc.getDocumentElement();
			return fromDomNode(root);
		} catch (Exception e) {
		    e.printStackTrace();
		}
		return null;
	}
	
	@SuppressWarnings("unchecked")
	protected static Experiment fromDomNode(Node root) {
		NamedNodeMap expAttrs = root.getAttributes();
		String id = getValue(expAttrs.getNamedItem("id").getNodeValue(), null);
		if (id == null) 
			throw new IllegalStateException();
		String description = getValue(expAttrs.getNamedItem("description").getNodeValue(), "");

		//settings
		int turnLimit = getIntValue(expAttrs.getNamedItem("turnLimit").getNodeValue(), DEFAULT_TURN_LIMIT);
		int turnTimeLimit = getIntValue(expAttrs.getNamedItem("turnTimeLimit").getNodeValue(), DEFAULT_TURN_TIME_LIMIT);
		boolean scramble = getBoolValue(expAttrs.getNamedItem("turnTimeLimit").getNodeValue(), DEFAULT_SCRAMBLE);
		boolean selfPlay = getBoolValue(expAttrs.getNamedItem("selfPlay").getNodeValue(), DEFAULT_SELFPLAY);
		boolean bothPos = getBoolValue(expAttrs.getNamedItem("bothPositions").getNodeValue(), DEFAULT_BOTHPOS);
		boolean parallel = getBoolValue(expAttrs.getNamedItem("parallel").getNodeValue(), DEFAULT_PARALLEL);
		int repeat = getIntValue(expAttrs.getNamedItem("repeat").getNodeValue(), DEFAULT_REPEAT);
		
		boolean analyze = DEFAULT_ANALYZE;
		if (expAttrs.getNamedItem("analyze") != null)
			analyze = getBoolValue(expAttrs.getNamedItem("analyze").getNodeValue(), DEFAULT_ANALYZE);
		
		//maps and players
		NodeList nodes = root.getChildNodes();
		ArrayList<Player> playerList = new ArrayList<Player>();
		ArrayList<Board> boardList = new ArrayList<Board>();
		for (int i = 0; i < nodes.getLength(); i++) {
			Node n = nodes.item(i);
			if (n.getNodeName().equals("map")) {
				String mapName = n.getTextContent();
				try {
					Board board = Board.fromFile(mapName + ".txt");
					boardList.add(board);
				} catch (FileNotFoundException e) {
					System.err.println("Experiment Parser: Unable to find map '" + mapName + "'");
				}
			}
			else if (n.getNodeName().equals("player")) {
				try {
					Class<Player> c = (Class<Player>) Class.forName(Player.class.getPackage().getName() + "." + n.getAttributes().getNamedItem("class").getNodeValue());
					Method method = c.getMethod("fromXml", Node.class, int.class, Color.class);
				    Player player = (Player) method.invoke(null, n, 0, Color.WHITE);
				    if (player == null)
				    	throw new IllegalStateException("failed to parse player");
				    else
				    	playerList.add(player);
				}
				catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
		
		Board[] boards = boardList.toArray(new Board[boardList.size()]);
		Player[] players = playerList.toArray(new Player[playerList.size()]);
		Experiment exp = new Experiment(id, description, players, boards, turnLimit, turnTimeLimit, scramble, selfPlay, bothPos, repeat);
		exp.setParallelMode(parallel);
		exp.setGameAnalysisMode(analyze);
		return exp;
	}
	
	private static String getValue(String value, String defaultValue) {
		if (value == null || value.equals("")) 
			return defaultValue;
		else
			return value;
	}
	
	private static int getIntValue(String value, int defaultValue) {
		if (value == null || value.equals("")) 
			return defaultValue;
		else {
			try {
				return Integer.parseInt(value);
			}
			catch (Exception e) {
				e.printStackTrace();
				return defaultValue;
			}
		}
	}
	
	private static boolean getBoolValue(String value, boolean defaultValue) {
		if (value == null || value.equals("")) 
			return defaultValue;
		else {
			try {
				return Boolean.parseBoolean(value);
			}
			catch (Exception e) {
				e.printStackTrace();
				return defaultValue;
			}
		}
	}
	
	@Root
	public static class Result {
		public final static int PLAYER1_WON = 1, PLAYER2_WON = -1, PLAYER_DRAW = 0, GAME_UNDECIDED = -9;
		public String name = "E";
		public String baseDir;
		//game index
		public int index = 0;
		@Attribute
		public int games = 0;
		@Attribute
		public double player1Score = 0;
		@Attribute
		public double player2Score = 0;
		@Attribute
		public int wins = 0;
		@Attribute
		public int draws = 0;
		@Element
		public int[] turns;
		@Element
		public int[] gameEnds;
		@Element
		public int turnLimit = 0;
		@Element
		public int turnTimeLimit = 0;
		@Element
		public boolean scramble = false;
		@Element
		public String board;
		@Element
		public String player1, player2;
		
		public void save(String file) throws Exception {
			toFile(file);
		}
		
		public void toFile(String file) throws Exception {
			File path = new File(file);
			File dir = new File(path.getParent());
			dir.mkdirs();
			Serializer serializer = new Persister();
			FileWriter writer = new FileWriter(file, true);
			serializer.write(this, writer);
			writer.write(String.format("\n"));
			writer.close();
		}
		
		public static Result fromFile(String file) throws Exception {
			Serializer serializer = new Persister();
			File source = new File(file);
			return serializer.read(Result.class, source);
		}
	}

	@Override
	public void onStart(Game g) {}

	@Override
	public void onPause(Game g) {}

	@Override
	public void onResume(Game g) {}

	@Override
	public void onDraw(Game g, Player[] winning) {}

	@Override
	public void onWin(Game g, Player p) {}

	@Override
	public synchronized void onGameOver(Game g) {
		notify();
	}

	@Override
	public void onPlayerDied(Game g, Player p) {}

	@Override
	public void onNextTurn(Game g) {
		if (DEBUG_OUTPUT)
			g.board.print();
	}

	@Override
	public void onMoveBegin(Game g) {}

	@Override
	public void onMoveEnd(Game g) {}
	
	public static final String FILE_EXT = "xml";
	public static FileFilter fileFilter = new FileFilter() {
		@Override
		public boolean accept(File f) {
			if (f.isDirectory())
				return true;
			
			String ext = "";
	        String name = f.getName();
	        int i = name.lastIndexOf('.');
	        if (i > 0 && i < name.length() - 1)
	            ext = name.substring(i + 1).toLowerCase();

	        if (ext.equals(FILE_EXT))
	        	return true;
	        else
	        	return false;
		}

		@Override
		public String getDescription() {
			return "Experiment files (*." + FILE_EXT + ")";
		}
	};

	@Override
	public void onPlayerMove(Game g, Board b, Player p, int m) {}
}
